import hashlib
import argparse
from dataclasses import dataclass
from datetime import date

ALPHA_CHAR_NUMBER = 26
DIGIT_CHAR_NUMBER = 10

MAX_PASSWORD_LENGTH = 256
PRINTABLE_AND_SEENABLE_CHAR_START = 33
PRINTABLE_AND_SEENABLE_CHAR_END = 126
LOWER_ALPHA_START = 97
LOWER_ALPHA_END = LOWER_ALPHA_START + ALPHA_CHAR_NUMBER - 1
UPPER_ALPHA_START = 65
UPPER_ALPHA_END = UPPER_ALPHA_START + ALPHA_CHAR_NUMBER - 1
DIGIT_CHAR_START = 48
DIGIT_CHAR_END = 48 + DIGIT_CHAR_NUMBER - 1


parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", action="store_true",                                                         help="show input")
parser.add_argument("name",                                                                                         help="your name.")
parser.add_argument("-p", "--phone",                                                                                help="your phone number.")
parser.add_argument("-b", "--birthday",                                                                             help="your birthday in format=YYYY-MM-DD.")
parser.add_argument("-s", "--salt",                                                                                 help="the salt you want to add in.")
parser.add_argument("-c", "--character-set", choices=['ascii', 'only-digits', 'digit-and-alpha'], default='ascii',  help="character set you want to use, ascii by default.")
parser.add_argument("-l", "--length", type=int, default=8,                                                          help="the length of password you want, max length is 256, 8 by default.")
args = parser.parse_args()
if args.length > MAX_PASSWORD_LENGTH:
    parser.print_help()
    exit(1)


@dataclass
class user_info():
    name : str
    phone : str | None
    birthday : date | None
    salt : str | None


    @classmethod
    def from_args(cls, args : argparse.Namespace):
        if args.birthday is not None:
            birthday_date = date.fromisoformat(args.birthday)
        else:
            birthday_date = None

        return cls(args.name, args.phone, birthday_date, args.salt)


    def hash(self) -> int:
        user_info_list = [self.name]
        if self.phone is not None:
            user_info_list.append(self.phone)

        if self.birthday is not None:
            user_info_list.append(self.birthday)

        if self.salt is not None:
            user_info_list.append(self.salt)

        hash_value_bytes = hashlib.md5(user_info_list.__str__().encode('utf-8')).digest()

        return int.from_bytes(hash_value_bytes, byteorder='little')


def map_int_to_digit_and_alpha(x : int) -> str:
    if x < DIGIT_CHAR_NUMBER:
        return chr(x + DIGIT_CHAR_START)
    elif x < DIGIT_CHAR_NUMBER + ALPHA_CHAR_NUMBER:
        return chr(x - DIGIT_CHAR_NUMBER + LOWER_ALPHA_START)
    else:
        return chr(x - DIGIT_CHAR_NUMBER - ALPHA_CHAR_NUMBER + UPPER_ALPHA_START)


character_set_mod_dict = {
    'ascii' : PRINTABLE_AND_SEENABLE_CHAR_END - PRINTABLE_AND_SEENABLE_CHAR_START + 1,
    'only-digits' : DIGIT_CHAR_NUMBER,
    'digit-and-alpha' : DIGIT_CHAR_NUMBER + ALPHA_CHAR_NUMBER * 2
}
mapping_func_dict = {
    'ascii' : lambda x : chr(x + PRINTABLE_AND_SEENABLE_CHAR_START),
    'only-digits' : lambda x : chr(x + DIGIT_CHAR_START),
    'digit-and-alpha' : map_int_to_digit_and_alpha
}
def generate_password(user_infomation : user_info, character_set : str, length : int) -> str:
    hash_value = user_infomation.hash()
    mod_value = character_set_mod_dict[character_set]
    mapping_func = mapping_func_dict[character_set]
    weights = [(2 ** (i - 1)) % i for i in range(1, length + 1)]
    int_list = [(hash_value + weight) % mod_value for weight in weights]
    str_list = [mapping_func(i) for i in int_list]

    return ''.join(str_list)


user_information = user_info.from_args(args)
if args.verbose:
    print(f'[Info] Your name is "{user_information.name}"')
    if user_information.phone is not None:
        print(f'[Info] Your phone is "{user_information.phone}"')

    if user_information.birthday is not None:
        print(f'[Info] Your birthday is {user_information.birthday}')

    if user_information.salt is not None:
        print(f'[Info] The salt you want to add is "{user_information.salt}"')

    print(f'[Info] The character set will be used is "{args.character_set}"')
    print(f'[Info] The length of password is {args.length}')

password = generate_password(user_information, args.character_set, args.length)
print(password)